Bienvenidos a la Actividad 1, donde pondremos en práctica todo lo aprendido durante el bloque 2. Esta actividad la realizaremos en clase, se terminará en casa (debería completarse en clase) y se entregará el día 8 de octubre.
Vamos a poner en práctica cuatro aspectos del procesamiento de imágenes:
La finalidad es sencilla. Se os dará una imagen, a color, que tiene varias tonalidades y que está pintada con círculos.
La actividad consiste en contar el número de círculos de la imagen.
Se evaluará de la siguiente manera:
No se aceptará el formato .ipynb Habilitaré una actividad en Canvas para que podáis subir ambos archivos.
En primer lugar, cargamos todos los paquetes/frameworks que nos van a hacer falta. Se recomienda visitar la web: https://scikit-image.org/ para ver todas las funcionalidades que permite Scikit Image.
# Paquetes necesarios para la realización de esta práctica (no son necesarios conocerlos ni entenderlos por ahora)
from skimage.io import imread
from skimage import transform as tf
import matplotlib.pyplot as plt
# Cargamos la función para convertir de RGB a Escala de grises
from skimage.color import rgb2gray
# Paquete y funciones para realizar una umbralización con Scikit-image
from skimage.filters import threshold_otsu, threshold_local, threshold_niblack, threshold_sauvola
# Paquetes necesarios para la morfología matemática
from skimage.morphology import erosion, dilation, opening, closing
# Elementos estructurales
from skimage.morphology import disk, diamond, ball, rectangle
# Estas dos funciones nos sirven para detectar los objetos dentro de una imagen binaria
from skimage.morphology import label
from skimage.measure import regionprops
# Defino una función para mostrar una imagen por pantalla con el criterio que considero más acertado
def imshow(img, title):
fig, ax = plt.subplots(figsize=(7, 7))
# El comando que realmente muestra la imagen
ax.imshow(img,cmap=plt.cm.gray)
# Para evitar que aparezcan los números en los ejes
ax.set_xticks([]), ax.set_yticks([])
ax.set_title(title)
plt.show()
Lo primero de todo, vamos a leer la imagen. Recuerda que hay que subir la imagen cada vez que inicies sesión en el notebook y que la ruta se mira haciendo botón derecho sobre el archivo.
Con lo cual, aquí vamos a hacer dos cosas:
Hacemos esto para luego posteriormente umbralizar la imagen en escala de grises.
from google.colab import files
from PIL import Image
uploaded = files.upload() #subir archivo
imagenpuntos = list(uploaded.keys())[0] #damos un nombre al archivo
imagen = Image.open(imagenpuntos)
imagen.show()
Saving Pintura_Puntos.jpg to Pintura_Puntos.jpg
plt.imshow(imagen)
plt.axis('off') #para ocultar los ejes
plt.show()
from skimage.color import rgb2gray
imagenpuntos_gris = rgb2gray(imagen) #mediante este comando lo paso a escala de grises
plt.imshow(imagenpuntos_gris, cmap="gray") #cuando uso la funcion plt se necesita indicar que mapee en gris"
plt.axis('off')
plt.show()
He cargado la imagen por medio del uso de la biblioteca PIL, tras esto he mostrado la imagen por pantalla con plt.show. Despues paso la imagen a escala de grises usando rgb2gray, y cunado lo muestro con plt lo mapeo en gris tambien.
# Os tendría que ir quedando una cosa así
Vamos a probar ahora diferentes métodos para umbralizar la imagen. Se pide en esta actividad:
1.UMBRALIZACION GLOBAL CON MÉTODO DE OTSU
# Calcula el umbral de Otsu
umbral_otsu = threshold_otsu(imagenpuntos_gris)
# Aplica la umbralización de Otsu a la imagen
imagen_umbralizada = imagenpuntos_gris > umbral_otsu
# Muestra la imagen original y la imagen umbralizada
plt.figure(figsize=(10, 4))
plt.subplot(1, 2, 1)
plt.imshow(imagenpuntos_gris, cmap='gray')
plt.title('Imagen en Escala de Grises')
plt.axis('off')
plt.subplot(1, 2, 2)
plt.imshow(imagen_umbralizada, cmap='gray')
plt.title('Imagen Umbralizada con Otsu')
plt.axis('off')
plt.show()
Lo primero que hemos hecho ha sido lanzar el comando para que me determine el umbral que va a usar el metodo global con otsu. Una vez realizado esto transforma la imagen teniendo en cuenta el umbral establecido. Sacamos por pantalla dos imagenes que me comparen la imagen en escala de grises sin treshold y la otra imagen umbralizada con Otsu.
# Calcula el umbral local utilizando threshold_local
umbral_local = threshold_local(imagenpuntos_gris, block_size=53)
# Aplica la umbralización local a la imagen
imagen_umbralizada_local = imagenpuntos_gris > umbral_local
# Muestra la imagen original y la imagen umbralizada local
plt.figure(figsize=(10, 4))
plt.subplot(1, 2, 1)
plt.imshow(imagenpuntos_gris, cmap='gray')
plt.title('Imagen en Escala de Grises')
plt.axis('off')
plt.subplot(1, 2, 2)
plt.imshow(imagen_umbralizada_local, cmap='gray')
plt.title('Imagen Umbralizada con Umbral Local')
plt.axis('off')
plt.show()
He umbralizado usando 53 pixeles de alrededor para que la haga media con esa cantidad porque si cogo un numero muy grande me estaria cogiendo practicamente toda la imagen y eso no lo queremos. He ido problando hasta que con un blocksize de 53 se parece a la imagen que se decia que era la mejor que se podia obtener.
Este tipo de umbralizacion es UMBRALIZACION ADAPTATIVA
import numpy as np
import matplotlib.pyplot as plt
histograma = plt.hist(imagenpuntos_gris.ravel(), bins=256, range=(0, 1))
He sacado el histograma
umbral_niblack = threshold_niblack(imagenpuntos_gris, window_size=55)
imagen_umbralizada_niblack = imagenpuntos_gris > umbral_niblack #umbralizamos la imagen
plt.figure(figsize=(10, 4))
<Figure size 1000x400 with 0 Axes>
<Figure size 1000x400 with 0 Axes>
plt.subplot(1, 2, 1)
plt.imshow(imagenpuntos_gris, cmap='gray')
plt.title('Imagen en Escala de Grises')
plt.axis('off')
plt.subplot(1, 2, 2)
plt.imshow(imagen_umbralizada_niblack, cmap='gray')
plt.title('Imagen Umbralizada con Niblack')
plt.axis('off')
plt.show()
He umbralizado con niblack que es un metodo ADAPTATIVO. He mostrado por pantalla la comparizacion de la imagen original en escala de grises frente a la umbralizada con niblack pudiendo observar como se ha converrtido la ia imagen en puntos blancos y negros (binaria).
umbral_sauvola = threshold_sauvola(imagenpuntos_gris, window_size=51)
imagen_umbralizada_sauvola = imagenpuntos_gris > umbral_sauvola #umbralizamos la imagen
plt.figure(figsize=(10, 4))
plt.subplot(1, 2, 1)
plt.imshow(imagenpuntos_gris, cmap='gray')
plt.title('Imagen en Escala de Grises')
plt.axis('off')
plt.subplot(1, 2, 2)
plt.imshow(imagen_umbralizada_niblack, cmap='gray')
plt.title('Imagen Umbralizada con Sauvola')
plt.axis('off')
plt.show()
He realizado la misma funcion de antes, pero he usado treshold_sauvola y he metido un windows size de 67. Este es un método ADAPTATIVO
Tenemos que rotar 180 grados la imagen que mejor deje definidos los circulos de la imagen original para asi poder contar de manera mas especifica y correcta. La imagen elegida es la que hemos aplicado el umbral local.
imagen_girada = np.rot90(np.rot90(imagenpuntos_gris))
plt.figure(figsize=(10, 4))
plt.subplot(1, 2, 2)
plt.imshow(imagen_girada, cmap="gray")
plt.title('Imagen Girada 180 Grados')
plt.axis('off')
plt.show()
# Calcula el umbral local utilizando threshold_local
umbral_local = threshold_local(imagen_girada, block_size=53)
# Aplica la umbralización local a la imagen
imagen_umbralizada_local_girada = imagen_girada > umbral_local
# Muestra la imagen original y la imagen umbralizada local
plt.figure(figsize=(10, 4))
plt.subplot(1, 2, 1)
plt.imshow(imagen_girada, cmap='gray')
plt.title('Imagen en Escala de Grises Girada')
plt.axis('off')
plt.subplot(1, 2, 2)
plt.imshow(imagen_umbralizada_local_girada, cmap='gray')
plt.title('Imagen Umbralizada con Umbral Local Girada')
plt.axis('off')
plt.show()
El proceso seguido ha sido girar la imagen en escala de grises y luego umbralizarla. Como no se si es lo mismo que girar directamente la umbralizada ahora voy a girar de manera directa:
imagen_girada2 = np.rot90(np.rot90(imagen_umbralizada_local))
plt.subplot(1, 2, 1)
plt.imshow(imagen_girada2, cmap='gray')
plt.title('Imagen Umbralizada con Umbral Local directamente Girada')
plt.axis('off')
plt.show()
Podemos observar que independientemente del orden en que hagamos la rotacion, si es antes de la umbralizacion o despues, se mantienen igual.
# Este es el mejor resultado que tendríais que alcanzar
Como se puede apreciar en la imagen hay varios elementos imperfectos:
Mediante el uso de morfología matemática (concretamente los cuatro operadores visto en clase) y los posibles elementos estructurales existentes, se pide:
Voy a probar con los 4 tipos de operadores matematicos (erosion, dilatacion, apertura y clausura). Despues los comapraré e intentaré sacar como conclusion con cual de los 4 metodos obtengo una imagen con mejor detalle de puntos para hacer un buen conteo de los mismos.
EROSION
import skimage.io
from skimage.morphology import erosion, dilation, opening, closing
from skimage import img_as_ubyte
kernel = np.ones((3, 3), np.uint8) # he leido que los kernels de menor tamaño pueden resaltar detalles mas finos. Considero que estamso ante un caso donde se necesita unalto grado de detalle y por esto mi kernel va a tener unos numeros bastante pequeños.
imagen_erosionada = erosion(imagen_umbralizada_local, kernel)
skimage.io.imshow(imagen_umbralizada_local)
skimage.io.imshow(imagen_erosionada)
skimage.io.show()
DILATACION
kernel = np.ones((1, 1), np.uint8) # he leido que los kernels de menor tamaño pueden resaltar detalles mas finos. Considero que estamso ante un caso donde se necesita unalto grado de detalle y por esto mi kernel va a tener unos numeros bastante pequeños.
imagen_dilation = dilation(imagen_umbralizada_local, kernel)
skimage.io.imshow(imagen_umbralizada_local)
skimage.io.imshow(imagen_dilation)
skimage.io.show()
la dilatacion me aumenta el tamaño de los circulos, por esto mismo he usado el tamaño de kernel mas pequeño posible ya que sino los circulos van a perder su controno y me quedare con una imagen completamente blanca.
APERTURA
kernel = np.ones((1, 1), np.uint8) # he leido que los kernels de menor tamaño pueden resaltar detalles mas finos. Considero que estamso ante un caso donde se necesita unalto grado de detalle y por esto mi kernel va a tener unos numeros bastante pequeños.
imagen_opening = opening(imagen_umbralizada_local, kernel)
skimage.io.imshow(imagen_umbralizada_local)
skimage.io.imshow(imagen_opening)
skimage.io.show()
Esta funcion primero erosiona y luego dilata, pero pienso que siguen quedando mejor definidos los puntos conla funcion de dilatacion a secas.
CLAUSURA
kernel = np.ones((1,1), np.uint8) # he leido que los kernels de menor tamaño pueden resaltar detalles mas finos. Considero que estamso ante un caso donde se necesita unalto grado de detalle y por esto mi kernel va a tener unos numeros bastante pequeños.
imagen_closing = closing(imagen_umbralizada_local, kernel)
skimage.io.imshow(imagen_umbralizada_local)
skimage.io.imshow(imagen_closing)
skimage.io.show()
Por medio de la apertura observo que la linea negra del centro no diferencia bien el tamaño de los circulos.
como conclusion pienso que el mejor operador morfologico matematico sobre esta imagen para que los putnos queden bien definidos en una dilatacion con el kernel de menor tamaño posible, de tal manera que elcontrono de las circunferencias no desaparezcan pero nos delimite de la mejor manera posible la linea negra central donde los contronos son mas gruesos.
Haciendo uso de las funcionalidades cargadas al principio, se pide hacer una función que:
Por último, ¿qué se podría hacer para asegurar que no se tienen en cuenta posibles errores en la umbralización como pequeños puntos o posible ruido que haya llegado hasta este punto?
imagen_binaria = imagen_dilation.astype(np.uint8)
print
(imagen_binaria)
array([[1, 1, 1, ..., 1, 1, 1],
[1, 1, 1, ..., 0, 0, 0],
[1, 1, 1, ..., 0, 0, 0],
...,
[1, 1, 1, ..., 1, 1, 1],
[1, 1, 1, ..., 1, 1, 1],
[1, 1, 1, ..., 1, 1, 1]], dtype=uint8)
len(regionprops(label(imagen_dilation))) #aqui obtengo la cantidad de circulos que me detecta usando labels y regionprops.
5680
print(imagen_binaria)
[[1 1 1 ... 1 1 1] [1 1 1 ... 0 0 0] [1 1 1 ... 0 0 0] ... [1 1 1 ... 1 1 1] [1 1 1 ... 1 1 1] [1 1 1 ... 1 1 1]]
plt.imshow(imagen_dilation, 'gray')
<matplotlib.image.AxesImage at 0x78432212bee0>
Esta sección no es obligatoria pero la pongo para aquellos que quieran saber "¿y ahora qué se haría?".
Lo que hemos hecho hasta ahora es:
Es decir, tenemos varios parámetros y tenemos una función que nos dice cuál es el número de puntos dada una imagen. Variando dichos parámetros, variará también el número de puntos, pero no parece haber una relación directa.
También no hay que olvidar que desconocemos el número de puntos (nunca se ha dicho, aunque siempre puedes contarlos), por lo que no podemos seguir un proceso de aprendizaje supervisado (tipo descenso del gradiente sobre los parámetros anteriores para encontrar el mejor resultado).
Pero lo que sí podemos hacer es iterar el valor de los parámetros para alcanzar un máximo de puntos (asumiendo que dicho máximo corresponderá con el mejor resultado). Esto suele hacerse cuando no sabemos exáctamente el resultado que esperamos.
En definitiva, ahora se buscaría realizar un proceso iterativo para encontrar el valor máximo del número de puntos. Para ello haría falta:
product del paquete itertools).Podría decirse que esa combinación de parámetros es la mejor.
from skimage import io, color, measure, morphology, filters
import itertools
def contar_puntos(imagen, umbral=None, tamaño_minimo=None):
# Realiza todas las operaciones necesarias en la imagen
imagen_gris = color.rgb2gray(imagen)
if umbral is None:
umbral = filters.threshold_otsu(imagen_gris)
imagen_binaria = imagen_gris > umbral
if tamaño_minimo is None:
tamaño_minimo = 10
imagen_morfologia = morphology.remove_small_objects(imagen_binaria, min_size=tamaño_minimo)
etiquetas = measure.label(imagen_morfologia, background=0)
regiones = measure.regionprops(etiquetas)
return len(regiones)
# Define los intervalos de parámetros que deseas explorar
intervalo_umbral = [0.1, 0.2, 0.3]
intervalo_tamaño_minimo = [5, 10, 20]
mejor_combinacion = None
max_puntos = 0
# Genera todas las combinaciones posibles de parámetros
for umbral, tamaño_minimo in itertools.product(intervalo_umbral, intervalo_tamaño_minimo):
# Aplica la función contar_puntos con la combinación de parámetros actual
puntos = contar_puntos(imagen, umbral, tamaño_minimo)
# Actualiza el número máximo de puntos y la mejor combinación de parámetros
if puntos > max_puntos:
max_puntos = puntos
mejor_combinacion = (umbral, tamaño_minimo)
print(f"Mejor combinación de parámetros: Umbral = {mejor_combinacion[0]}, Tamaño mínimo = {mejor_combinacion[1]}")
print(f"Número máximo de puntos detectados: {max_puntos}")
Mejor combinación de parámetros: Umbral = 0.2, Tamaño mínimo = 5 Número máximo de puntos detectados: 2503